﻿using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using Equation.Enterprise.Utilities.DesignByContract;
using Equation.Enterprise.Utilities.Log;
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;

namespace Equation.Enterprise.Utilities.Caching
{
    /// <summary>
    /// InMemoryCache handles basic caching functionalities for retrieving data from repository
    /// To use it, create an InMemoryCache object then retrieve your data by passing your repository 
    /// into the Get method or GetAsynch method
    /// Config required to your config file
    ///  <configSections>
    ///      <section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching" />
    ///  </configSections>
    ///  <cachingConfiguration defaultCacheManager="Default Cache Manager">
    ///      <cacheManagers>
    ///           <add name="Default Cache Manager" type="Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager, Microsoft.Practices.EnterpriseLibrary.Caching, Version=5.0.505.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" expirationPollFrequencyInSeconds="60" maximumElementsInCacheBeforeScavenging="1000000" numberToRemoveWhenScavenging="10" backingStoreName="inMemory" />
    ///      </cacheManagers>
    ///  </cachingConfiguration>
    /// </summary>
    public class InMemoryCache : ICacheService
    {
        private ICacheManager _cacheManager;
        private static readonly object SyncLock = new object();
        private readonly string _cacheDirectory = Environment.CurrentDirectory + "\\Equation.Cache\\";
        private readonly string _cacheManagerName = "Default Cache Manager";
        private const string CacheFileExtension = ".dat";
        private const string LogFileName = "Equation.Cache.log";
        private const string ErrorFileName = "Equation.Cache.Error.log";
        private static readonly BinaryFormatter BinaryFormatter = new BinaryFormatter();
        private const string LogKeyInfo = "InMemoryCache";
        private const string LogKeyError = "InMemoryCacheError";


        public InMemoryCache(): this(string.Empty, string.Empty)
        {
        }

        public InMemoryCache(string cacheDirectory) : this(cacheDirectory, string.Empty)
        {
        }

        public InMemoryCache(string cacheDirectory, string cacheManagerName)
        {
            if (!string.IsNullOrEmpty(cacheDirectory))
                this._cacheDirectory = cacheDirectory;

            if (!string.IsNullOrEmpty(cacheManagerName))
                this._cacheManagerName = cacheManagerName;

            CreateCacheManager();

            LoggerFactory.Register(LogKeyInfo, LogFileName, LogLevel.Info, true);
            LoggerFactory.Register(LogKeyError, ErrorFileName, LogLevel.Error, false);
        }

        private void CreateCacheManager()
        {
            this._cacheManager = CacheFactory.GetCacheManager(this._cacheManagerName);
        }

        public T Get<T>(string cacheId, Func<T> getItemCallBack, int cacheDurationMinutes) where T : class
        {
            return Get(cacheId, getItemCallBack, cacheDurationMinutes, false);
        }

        public T Get<T>(string cacheId, Func<T> getItemCallBack, int cacheDurationMinutes, bool withSerialization) where T : class
        {
            cacheId = cacheId.ToLower();
            T item = null;

            if (!string.IsNullOrEmpty(cacheId))
                item = _cacheManager.GetData(cacheId) as T;

            if (item == null)
                item = GetFromRepository<T>(cacheId, getItemCallBack, cacheDurationMinutes, withSerialization, item);

            return item;
        }

        public T GetAsynch<T>(string cacheId, Func<T> getItemCallBack, int cacheDurationMinutes, bool withSerialization) where T : class
        {
            T item = null;
            bool refresh = false;

            cacheId = cacheId.ToLower();

            //Refresh cache in case of expired
            if (CacheExpiration.Instance.IsExpired(cacheId))
            {
                lock(SyncLock)
                {
                    if (CacheExpiration.Instance.IsExpired(cacheId))
                    {
                        CacheExpiration.Instance.CachedItems[cacheId] = DateTime.Now.AddMinutes(cacheDurationMinutes);
                        refresh = true;
                    }
                }
            }

            if (refresh)
            {
                Logger.LogInfo(cacheId + " expired", LogKeyInfo);
                var getTask = new GetTask<T>(cacheId, getItemCallBack, 525600 + cacheDurationMinutes, withSerialization, item);
                ThreadPool.QueueUserWorkItem(new WaitCallback(GetFromRepository<T>), getTask);
            }

            //Check cache for item
            if (!string.IsNullOrEmpty(cacheId))
                item = _cacheManager.GetData(cacheId) as T;

            if (item==null)
                Logger.LogInfo(cacheId + " not found in cache, returning NULL item to caller");

            return item;
        }

        #region Private Methods

        private T GetFromRepository<T>(string cacheId, Func<T> getItemCallBack, int cacheDurationMinutes, bool withSerialization, T item) where T : class
        {
            string objectSubDirectory = GetObjectSubDirectory<T>();

            //Get item call back
            try
            {
                Logger.LogInfo(cacheId + " getItemCallback started", LogKeyInfo);
                item = getItemCallBack();
                Logger.LogInfo(cacheId + " getItemCallback completed", LogKeyInfo);
            }
            catch (Exception exception)
            {
                Logger.LogError(exception, LogKeyError);
            }

            //Get item successfully
            //Serialize this item if withSerialization is true
            if (item != null)
            {
                if (withSerialization)
                {
                    var taskData = new SerializationTask(cacheId, item, objectSubDirectory);
                    ThreadPool.QueueUserWorkItem(new WaitCallback(CreateSerializedCopy), taskData);
                }
            }
            else //Get item unsuccessfully, try to get from serializedCopy
                if (withSerialization)
                    item = (T) GetSerializedCopy(cacheId, GetObjectSubDirectory<T>());

            //Add item to cache
            if (item!=null)
            {
                AddToCache(cacheId, item, CacheItemPriority.Normal, null, cacheDurationMinutes);
            }
            else
                Logger.LogInfo(cacheId + " not retrieved from repository, cannot cache item", LogKeyInfo);
                

            return item;
        }

        private void GetFromRepository<T>(Object obj) where T:class
        {
            var getTask = (GetTask<T>) obj;
            GetFromRepository<T>(getTask.CacheId, 
                                    getTask.GetItemCallBack, 
                                    getTask.CacheDurationMinutes,
                                    getTask.WithSerialization, 
                                    getTask.Item);
        }

        private void CreateSerializedCopy(object obj)
        {
            var taskInfo = obj as SerializationTask;

            if (taskInfo == null) return;
            
            string cacheId = taskInfo.CacheId;
            object item = taskInfo.Item;
            string objectPath = _cacheDirectory;
            if (taskInfo.ObjectSubDirectory != "")
                objectPath += taskInfo.ObjectSubDirectory + @"\";

            try
            {
                if (item != null)
                {
                    var directory = new DirectoryInfo(objectPath);
                    if (!directory.Exists)
                        directory.Create();

                    using (var fileStream = new FileStream(objectPath + cacheId + CacheFileExtension, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
                    {
                        BinaryFormatter.Serialize(fileStream, item);
                    }
                }
            }
            catch (IOException ioException)
            {
                Logger.LogError(ioException, LogKeyError);
            }
            catch (SerializationException serializationException)
            {
                Logger.LogError(serializationException, LogKeyError);
            }
            catch (Exception exception)
            {
                Logger.LogError(exception, LogKeyError);
            }
        }

        private object GetSerializedCopy(string cacheId, string objectSubDirectory)
        {
            string objectPath = _cacheDirectory;
            if (!string.IsNullOrEmpty(objectSubDirectory))
                objectPath += objectSubDirectory + @"\";

            object item = null;

            try
            {
                var directoryInfo = new DirectoryInfo(objectPath);
                if (!directoryInfo.Exists)
                    directoryInfo.Create();

                using (var fileStream = new FileStream(objectPath + cacheId + CacheFileExtension, FileMode.Open, FileAccess.Read, FileShare.None))
                {
                    item = BinaryFormatter.Deserialize(fileStream);
                }
            }
            catch (IOException ioException)
            {
                Logger.LogError(ioException, LogKeyError);
            }
            catch (SerializationException serializationException)
            {
                Logger.LogError(serializationException, LogKeyError);
            }
            catch (Exception exception)
            {
                Logger.LogError(exception, LogKeyError);
            }

            return item;
        }

        private void AddToCache(string cacheId, object item, CacheItemPriority priority, ICacheItemRefreshAction refreshAction, int cacheDurationMinutes)
        {
            if (cacheDurationMinutes == 0 || string.IsNullOrEmpty(cacheId)) return;

            _cacheManager.Add(cacheId, item, priority, refreshAction, new AbsoluteTime(TimeSpan.FromMinutes(cacheDurationMinutes)));
        }

        private string GetObjectSubDirectory<T>()
        {
            string objectSubDirectory = typeof (T).ToString();
            if (objectSubDirectory.StartsWith("System.Collections."))
                objectSubDirectory = objectSubDirectory.Substring(objectSubDirectory.IndexOf("[") + 1).Replace("]", "");

            return objectSubDirectory;
        }

        #endregion
    }

    public class GetTask<T>
    {
        public string CacheId;
        public Func<T> GetItemCallBack;
        public int CacheDurationMinutes;
        public bool WithSerialization;
        public T Item;

        public GetTask(string cacheId, Func<T> getItemCallBack, int cacheDurationMinutes, bool withSerialization, T item)
        {
            CacheId = cacheId;
            GetItemCallBack = getItemCallBack;
            CacheDurationMinutes = cacheDurationMinutes;
            WithSerialization = withSerialization;
            Item = item;
        }
    }

    public class SerializationTask
    {
        public string CacheId;
        public object Item;
        public string ObjectSubDirectory;

        public SerializationTask(string cacheId, object item, string objectSubDirectory)
        {
            CacheId = cacheId;
            Item = item;
            ObjectSubDirectory = objectSubDirectory;
        }
    }
}
